#include "httpserver.h"
#include "ircclient.h"
#include "connectiondata.h"
#include "global.h"

#include <QRegularExpression>
#include <QDirIterator>
#include <QDateTime>
#include <QHostAddress>
#include <QTcpSocket>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QMutex>
#include <QFile>
#include <QDir>

const char CRITICAL_ERROR[] {"<title>Critical error</title><center><h1>NOT FOUND</H1><p>Maybe it's compile time error</p></center>"};

HttpServer::HttpServer(const QString &address, quint16 port, const QString& dataFolder,
                       const QString& serviceName, const QString& serviceEmoji, bool ajaxIsDisabled, QObject *parent) :
    QObject(parent),
    m_TcpServer(new QTcpServer),
    m_serviceName(serviceName),
    m_dataFolder(dataFolder),
    m_ajaxIsDisabled(ajaxIsDisabled)
{
    if (not m_TcpServer->listen(QHostAddress(address), port)) {
        throw std::runtime_error("HttpServer not binded at " +
                                 address.toStdString() + " : " + QString::number(port).toStdString());
    }
    consoleLog("http://" +address + ":" + QString::number(port) + "/");
    if (m_ajaxIsDisabled) {
        consoleLog("JavaScript on webpages removed and AJAX disabled!");
    }

    if (not QFile::exists(m_dataFolder+"main_page.txt")) {
        QFile mp(m_dataFolder+"main_page.txt");
        if (mp.open(QIODevice::WriteOnly)) {
            mp.write("# Main page file.\n"
                     "# HTML is supported. For line breaks, use <br> or <p></p>.\n\n"
                     "<center>\n"
                     "# Your images from \"" + m_dataFolder.toUtf8() + "custom_images\" must have URL \"/~images/\"\n"
                     "<img src=\"/~images/example.png\">\n"
                     "<p><span style=\"color: gray\">local time:</span> " +LOCAL_TIME_MARKER_FOR_MAIN_PAGE.toUtf8()+ "<br>\n"
                     "<span style=\"color: gray\">daily requests:</span> " +DAILY_REQUESTS_COUNTER_VALUE_FOR_MAIN_PAGE.toUtf8()+ "</p>\n"
                     "</center>\n");
            mp.close();
        }
        else {
            throw std::runtime_error("main_page.txt not exist and creating failed");
        }
    }

    QDir dir {m_dataFolder};
    if (not dir.cd("custom_images")) {
        if (dir.mkdir("custom_images")) {
            dir.cd("custom_images");
            QFile examplePng(dir.path()+global::slash+"example.png");
            if (examplePng.open(QIODevice::WriteOnly)) {
                QFile nativePng("://html/custom_img_example.png");
                if (nativePng.open(QIODevice::ReadOnly)) {
                    examplePng.write(nativePng.readAll());
                    nativePng.close();
                    examplePng.close();
                }
            }
        } else {
            consoleLog("Creating folder \"custom_images\" failed");
        }
    }

    m_serviceButton = HTML_LEFT_MENU_MAIN_POINT;
    replaceTag(m_serviceButton, "EMOJI", serviceEmoji);
    replaceTag(m_serviceButton, "SERVICE_NAME", serviceName);

    connect (m_TcpServer, &QTcpServer::newConnection, this, &HttpServer::acceptor);
}

HttpServer::~HttpServer()
{
    m_TcpServer->close();
    m_TcpServer->deleteLater();
}

QString HttpServer::convertToClickableLink(const QString &httpLine)
{
    QString result;
    if (not httpLine.contains(QRegularExpression("http.?://"))) return result;

    QString displayedName {httpLine};
    displayedName.remove(QRegularExpression("http.?://(www\\.)?"));
    displayedName.remove(QRegularExpression("/$"));
    result = "<a href=\"" + httpLine + "\"> " + displayedName + " </a>";
    return result;
}

std::pair<QString, QString> HttpServer::splitUserNameAndMessage(const QString &rawLine)
{
    std::pair<QString, QString> result;

    QString nick {rawLine};
    nick.remove(QRegularExpression("\\]\\s.*$"));
    nick.remove(QRegularExpression("^\\["));
    if (nick.isEmpty()) {
        return result;
    }
    nick = nick.toHtmlEscaped();

    // long nicks
    if (nick.size() > MAX_NICKNAME_LENGTH_WITHOUT_WBR) {
        int lastWbr = 0;
        for (int i = 0; i < nick.size(); i++) {
            if (i-lastWbr > MAX_NICKNAME_LENGTH_WITHOUT_WBR) {
                nick.insert(i, "<wbr>");
                lastWbr = i;
            }
        }
    }

    QString text {rawLine};
    text.remove(QRegularExpression("^\\[[^\\s]*\\]\\s"));
    if (text.isEmpty()) {
        return result;
    }
    text = text.toHtmlEscaped();

    // http links
    bool linksFound {false};
    while (QRegularExpression("(^|\\s)http.?://").match(text).hasMatch()) {
        if (not linksFound) linksFound = true;
        int pos = text.indexOf(QRegularExpression("(^|\\s)http.?://"));
        if (pos == -1) {
            consoleLog("Bug! HttpServer.cpp while (QRegularExpression(\"(^|\\s)http.?://\").match(text).hasMatch())");
            break;
        }
        QString rawLink {text};
        rawLink.remove(0, pos);
        if (rawLink.startsWith(' ')) {
            rawLink.remove(0,1);
        }
        int space = rawLink.indexOf(' ');
        if (space > 0) {
            rawLink.remove(space, rawLink.size()-space);
        }
        text.replace(rawLink, convertToClickableLink(rawLink));
    }
    // long lines
    int space = 0;
    bool nbTag = false; // For safe HTML tags like a &it; via <wbr>!
    bool isHref = false;
    for (int i = 0; i < text.size(); i++) {
        if (text[i] == ' ') {
            space = i;
            if (isHref) {
                isHref = false;
            }
            else {
                if (text.indexOf("href=\"http", i+1) == i+1) {
                    isHref = true;
                }
            }
        }

        if (nbTag and text[i-1] == ';') {
            nbTag = false;
        }
        if (text.indexOf(QRegularExpression("(\\&amp;|\\&lt;|\\&gt;|\\&quot;).*"), i) == i) {
            nbTag = true;
        }

        if (not isHref and i-space > MAX_MESSAGE_LENGTH_WITHOUT_WBR and not nbTag) {
            text.insert(i, "<wbr>");
            space = i;
        }
    }
    if (linksFound) text.replace(" </a>", "</a>"); // delete whitespace in links end

    result.first = nick;
    result.second = text;
    return result;
}

void HttpServer::consoleLog(const QString &message)
{
    qInfo().noquote() << "[WEB_UI]" << message;
}

void HttpServer::debugLog(const QString &req)
{
    QFile log(m_dataFolder + "debug.log");
    if (log.open(QIODevice::WriteOnly | QIODevice::Append)) {
        log.write(QDateTime::currentDateTime().toString().toUtf8() + ":\n" + req.toUtf8() + "\n\n");
        log.close();
    }
}

void HttpServer::acceptor()
{
    QTcpSocket* socket = m_TcpServer->nextPendingConnection();
    static uint sockcount = 0;
    if (++sockcount < 20)
    {
        connect(socket, &QTcpSocket::readyRead, this, &HttpServer::reader);
        connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater);
        connect(socket, &QTcpSocket::disconnected, [&]() {--sockcount;} );
    }
    else
    {
        socket->close();
        socket->deleteLater();
    }
}

void HttpServer::reader()
{
    QTcpSocket* socket = static_cast<QTcpSocket*>(sender());
    QString request = socket->read(BUFFER_SIZE);
    if (not request.startsWith("GET") and not request.startsWith("HEAD")) {
        if (socket->isOpen()) {
            socket->write("Your request has been rejected!\n");
            socket->disconnectFromHost();
        }
        return;
    }

    bool isHeadRequest = false;
    if (request.startsWith("HEAD ")) {
        isHeadRequest = true;
    }

    QString urlPath = getRequestPath(request);
    if (urlPath == "/favicon.ico") {
        QString eTag = global::getValue(request, "If-None-Match", global::eHttpHeader);
        if (eTag == HTTP_ACTUAL_ETAG) {
            if (socket->isOpen()) socket->write(HEADER_304.toUtf8());
        }
        else {
            QFile icon("://html/favicon.ico");
            if (icon.open(QIODevice::ReadOnly)) {
                QByteArray file = icon.readAll();
                icon.close();
                QString header = HEADER_ICO;
                replaceTag(header, "SIZE", QString::number(file.size()));
                if (socket->isOpen()) {
                    socket->write(header.toUtf8());
                    if (not isHeadRequest) socket->write(file);
                }
            }
        }
    }
    else if (urlPath == "/") {
        ++m_requestCounterPlainInfo;
        writeMainPage(socket, isHeadRequest);
    }
    else if (urlPath == "/newmessage.mp3" and not m_ajaxIsDisabled) {
        QString eTag = global::getValue(request, "If-None-Match", global::eHttpHeader);
        if (eTag == HTTP_ACTUAL_ETAG) {
            if (socket->isOpen()) socket->write(HEADER_304.toUtf8());
        }
        else {
            QFile mp3("://html/newmessage.mp3");
            if (mp3.open(QIODevice::ReadOnly)) {
                QByteArray file = mp3.readAll();
                mp3.close();
                QString header = HEADER_MP3;
                replaceTag(header, "SIZE", QString::number(file.size()));
                if (socket->isOpen()) {
                    socket->write(header.toUtf8());
                    if (not isHeadRequest) socket->write(file);
                }
            }
        }
    }
    else if (urlPath == "/style.css") {
        QString eTag = global::getValue(request, "If-None-Match", global::eHttpHeader);
        if (eTag == HTTP_ACTUAL_ETAG) {
            if (socket->isOpen()) socket->write(HEADER_304.toUtf8());
        }
        else {
            QFile css("://html/style.css");
            if (css.open(QIODevice::ReadOnly)) {
                QByteArray file = css.readAll();
                css.close();
                QString header = HEADER_CSS;
                replaceTag(header, "SIZE", QString::number(file.size()));
                if (socket->isOpen()) {
                    socket->write(header.toUtf8());
                    if (not isHeadRequest) socket->write(file);
                }
            }
        }
    }
    else if (urlPath.endsWith(".svg")) {
        QString eTag = global::getValue(request, "If-None-Match", global::eHttpHeader);
        if (eTag == HTTP_ACTUAL_ETAG) {
            if (socket->isOpen()) socket->write(HEADER_304.toUtf8());
        }
        else {
            QFile svg("://html"+urlPath);
            if (svg.open(QIODevice::ReadOnly)) {
                QByteArray file = svg.readAll();
                svg.close();
                QString header = HEADER_SVG;
                replaceTag(header, "SIZE", QString::number(file.size()));
                if (socket->isOpen()) {
                    socket->write(header.toUtf8());
                    if (not isHeadRequest) socket->write(file);
                }
            }
            else {
                if (socket->isOpen()) {
                    socket->write(HEADER_404.toUtf8());
                    if (not isHeadRequest) writeErrorPage(socket);
                }
            }
        }
    }
    else if (urlPath == "/realtimechat.js" and not m_ajaxIsDisabled) {
        QFile js("://html/realtimechat_js");
        if (js.open(QIODevice::ReadOnly)) {
            QByteArray file = js.readAll();
            js.close();
            QString header = HEADER_JS;
            replaceTag(header, "SIZE", QString::number(file.size()));
            if (socket->isOpen()) {
                socket->write(header.toUtf8());
                if (not isHeadRequest) socket->write(file);
            }
        }
    }
    else if (urlPath.startsWith("/ajax/")) {
        if (m_ajaxIsDisabled) {
            writeErrorJson(socket, "AJAX disabled");
        } else {
            ++m_requestCounterAjax;
            writeAjaxAnswer(socket, urlPath, isHeadRequest);
        }
    }
    else if (urlPath.startsWith("/~realtime/")) {
        if (m_ajaxIsDisabled) {
            writeErrorPage(socket, "DISABLED"
                                   "<p>No JS, no AJAX, no real time reading!</p>");
        } else {
            ++m_requestCounterPlainInfo;
            writeRealTimeChatPage(socket, urlPath, isHeadRequest);
        }
    }
    else if (urlPath.startsWith("/~images/")) {
        writeCustomPicture(socket, urlPath, isHeadRequest);
    }
    else {
        ++m_requestCounterPlainInfo;
        writeRegularPage(socket, urlPath, isHeadRequest);
    }
    socket->disconnectFromHost();
}

void HttpServer::ircBotFirstInfo(QString server, QStringList channels)
{
    for (const auto &c: channels) {
        if (c.isEmpty()) continue;
        m_servers[server][c] = QStringList();
    }
}

void HttpServer::ircUsersOnline(QString server, QString channel, QStringList users)
{
    if (server.isEmpty()) return;
    if (channel.isEmpty()) return;
    QStringList sortedNicknames;
    QStringList ownersNicks;
    QStringList operNicks;
    QStringList halfopNicks;
    QStringList adminNicks;
    QStringList voicedNicks;
    QStringList plainNicks;
    for (auto rawOneNick: users) {
        rawOneNick = rawOneNick.toHtmlEscaped();
        if (rawOneNick.startsWith('~')) {
            ownersNicks.push_back(rawOneNick);
        }
        else if (rawOneNick.startsWith('@')) {
            operNicks.push_back(rawOneNick);
        }
        else if (rawOneNick.startsWith('%')) {
            halfopNicks.push_back(rawOneNick);
        }
        else if (rawOneNick.startsWith('&')) {
            adminNicks.push_back(rawOneNick);
        }
        else if (rawOneNick.startsWith('+')) {
            voicedNicks.push_back(rawOneNick);
        }
        else {
            plainNicks.push_back(rawOneNick);
        }
    }
    if (not ownersNicks.isEmpty()) {
        std::sort(ownersNicks.begin(), ownersNicks.end());
        sortedNicknames += ownersNicks;
    }
    if (not operNicks.isEmpty()) {
        std::sort(operNicks.begin(), operNicks.end());
        sortedNicknames += operNicks;
    }
    if (not halfopNicks.isEmpty()) {
        std::sort(halfopNicks.begin(), halfopNicks.end());
        sortedNicknames += halfopNicks;
    }
    if (not adminNicks.isEmpty()) {
        std::sort(adminNicks.begin(), adminNicks.end());
        sortedNicknames += adminNicks;
    }
    if (not voicedNicks.isEmpty()) {
        std::sort(voicedNicks.begin(), voicedNicks.end());
        sortedNicknames += voicedNicks;
    }
    if (not plainNicks.isEmpty()) {
        std::sort(plainNicks.begin(), plainNicks.end());
        sortedNicknames += plainNicks;
    }
    sortedNicknames.removeAll("");
    m_servers[server][channel] = sortedNicknames;
}

void HttpServer::ircChannelTopic(QString server, QString channel, QString topic)
{
    m_channelsTopic[server][channel] = topic;
}

void HttpServer::ircServerOnline(QString server, quint8 status)
{
    if (server.isEmpty()) return;
    bool online = status;
    m_serversOnline[server] = online;
}

void HttpServer::ircBotNick(QString server, QString nickname)
{
    m_botNick[server] = nickname;
}

void HttpServer::ircMessageCache(QString server, QString channel, QString nick, QString text)
{
    QString channelId {server+channel};
    if (not m_messageCache.contains(channelId)) return;
    // remove timed out session
    if (QDateTime::currentMSecsSinceEpoch() - MSECS_TO_AUTOREMOVE_MESSAGES_FROM_BUFFER >
            m_messageCache[channelId]->getLastPing())
    {
        consoleLog("Message caching disabled for "+server+"/#"+channel+". No active reader.");
        m_messageCache.remove(channelId);
        return;
    }
    else {
        auto processedMsg {splitUserNameAndMessage("["+nick+"] " + text)};
        m_messageCache[channelId]->saveNewMessage(processedMsg.first, processedMsg.second);
    }
}

QString HttpServer::getRequestPath(const QString &req)
{
    if (req.isEmpty()) return QString();

    QString result(req);

    int begin = result.indexOf(' ');
    if (begin == -1) return QString();

    result.remove(0, begin+1);

    int space = result.indexOf(' ');
    int size = result.size();
    result.remove(space, size-space);
    result = QByteArray::fromPercentEncoding(result.toUtf8());

    return result;
}

QString HttpServer::getWordFromPath(const QString &path)
{
    QString result {path};
    result.remove(QRegularExpression("\\?.*$")); // any actions like a ?toSearch=
    if (result.startsWith('/')) {
        result.remove(QRegularExpression("^/"));
    }
    result.remove(QRegularExpression("/.*$"));
    return result;
}

void HttpServer::writeMainPage(QTcpSocket *socket, bool isHeadRequest)
{
    auto renderStart = QDateTime::currentMSecsSinceEpoch();

    QFile main("://html/main.html");
    QString page;
    if (main.open(QIODevice::ReadOnly)) {
        page = main.readAll();
        main.close();
    }
    else {
        if (socket->isOpen()) {
            socket->write(HEADER_404.toUtf8());
            if (not isHeadRequest) socket->write(CRITICAL_ERROR);
        }
    }

    replaceTag(page, "PAGE_TITLE", m_serviceName + " | IRC logger");

    //// Left menu compilation
    QString htmlServersSectionS;
    for (const auto &s: m_servers) {
        if (s.first.isEmpty()) continue; // empty server name?

        QString htmlServersSection = HTML_SERVER_SECTION;
        replaceTag(htmlServersSection, "ABOUT_SERVER", "/"+s.first);
        replaceTag(htmlServersSection, "SERVER_NAME", s.first);

        QString htmlChannelLineS;
        for (const auto &c: s.second) {
            QString htmlChannelLine;
            htmlChannelLine = HTML_SERVER_SECTION_CHANNEL;
            replaceTag(htmlChannelLine, "CHANNEL_NAME", c.first);

            QString channelNameForUrl {c.first};
            channelNameForUrl.remove('#');
            QString channelLink = "/" + global::toLowerAndNoSpaces(s.first) + "/" + channelNameForUrl;
            replaceTag(htmlChannelLine, "CHANNEL_LINK", channelLink);

            htmlChannelLineS += htmlChannelLine;
        }
        replaceTag(htmlServersSection, "CHANNELS", htmlChannelLineS);

        bool online {false};
        for (const auto &srv: m_serversOnline) {
            if (srv.first == s.first) {
                online = srv.second;
                break;
            }
        }
        if (online) {
            replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_ONLINE_MARKER);
        } else {
            replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_OFFLINE_MARKER);
        }
        htmlServersSectionS += htmlServersSection;
    }
    QString serviceButtonSelected {m_serviceButton};
    serviceButtonSelected.replace("class=\"left_menu__mainitem\"",
                                  "class=\"left_menu__mainitem\" style=\"opacity: 1\"");
    htmlServersSectionS.push_front(serviceButtonSelected);
    replaceTag(page, "SERVERS_SECTION", htmlServersSectionS);
    page.remove(QRegularExpression("<div class=\"main_header\">.*<!-- main_middle -->", QRegularExpression::DotMatchesEverythingOption));

    QString payloadBlock = HTML_PAYLOAD_ABOUT;
    payloadBlock.remove("<span style=\"color: green; display: block; font-size: 24px; text-align: center;\">{{ABOUT_TITLE}}</span><br>\n");

    QString aboutBlock;
    QFile mp(m_dataFolder+global::slash+"main_page.txt");
    if (mp.open(QIODevice::ReadOnly)) {
        QString rbuffer = mp.readLine();
        while (not rbuffer.isEmpty()) {
            if (rbuffer.startsWith('#')) {
                rbuffer = mp.readLine();
                continue;
            }
            removeBrakelineSymbols(rbuffer);
            if (not rbuffer.isEmpty()) {
                aboutBlock += rbuffer;
            }
            rbuffer = mp.readLine();
        }
        aboutBlock.replace(LOCAL_TIME_MARKER_FOR_MAIN_PAGE, QTime::currentTime().toString(Qt::DateFormat::ISODate));
        QString reqCounterStr = QString::number(*m_requestCounterPlainInfo.value());
        if (not m_ajaxIsDisabled)
        {
            reqCounterStr += " (html) / " + QString::number(*m_requestCounterAjax.value()) + " (ajax)";
        }
        aboutBlock.replace(DAILY_REQUESTS_COUNTER_VALUE_FOR_MAIN_PAGE, reqCounterStr);
    }
    else {
        aboutBlock = "No information provided";
    }
    replaceTag(payloadBlock, "ABOUT_TEXT", aboutBlock);
    replaceTag(page, "PAYLOAD_BLOCK", payloadBlock);

    //// Footer
    replaceTag(page, "VERSION", global::IRCABOT_VERSION);
    replaceTag(page, "COPYRIGHT_YEAR", global::COPYRIGHT_YEAR);

    //// Finish
    replaceTag(page, "RENDERING_TIMER", QString::number(QDateTime::currentMSecsSinceEpoch() - renderStart));
    QString mainHeader = HEADER_HTML;
    replaceTag(mainHeader, "SIZE", QString::number(QByteArray(page.toUtf8()).size()));

    if (socket->isOpen()) {
        socket->write(mainHeader.toUtf8());
        if (not isHeadRequest) socket->write(page.toUtf8());
    }
}

void HttpServer::writeCustomPicture(QTcpSocket *socket, QString &urlPath, bool isHeadRequest)
{
    QString fileName {urlPath};
    fileName.remove(QRegularExpression("^/~images/"));

    QFile f {m_dataFolder+global::slash+"custom_images"+global::slash+fileName};
    if (not f.exists()) {
        writeErrorPage(socket, fileName + "<p>IMAGE NOT FOUND</p>");
        return;
    }
    QByteArray file;
    if (f.open(QIODevice::ReadOnly)) {
        file = f.readAll();
        f.close();
    }
    else {
        writeErrorPage(socket, "FORBIDDEN");
        return;
    }

    QString picHeader = HEADER_IMG;
    QString fileType {fileName};
    fileType.remove(QRegularExpression(".*\\."));
    replaceTag(picHeader, "TYPE", fileType);
    replaceTag(picHeader, "SIZE", QString::number(file.size()));
    if (socket->isOpen()) {
        socket->write(picHeader.toUtf8());
        if (not isHeadRequest) socket->write(file);
    }
}

void HttpServer::writeErrorPage(QTcpSocket *socket, const QString& text)
{
    if (socket->isOpen()) {
        socket->write(HEADER_404.toUtf8());
        socket->write("<title>404</title>"
                      "<center>"
                      "<p><h1>"+text.toUtf8()+"</H1></p>"
                      "<p><h3>[<a href=\"/\" style=\"text-decoration: none\">main page</a>]</h3></p>"
                      "</center>");
    }
}

void HttpServer::writeErrorJson(QTcpSocket * socket, const QString &text)
{
    QString header = HEADER_JSON;
    QByteArray body {
        "{\"status\": false, \"message\": \"" + text.toUtf8() + "\"}"
    };
    replaceTag(header, "SIZE", QString::number(body.size()));
    socket->write(header.toUtf8());
    socket->write(body);
}

void HttpServer::removeBrakelineSymbols(QString &line)
{
    line.remove('\r');
    line.remove('\n');
    line.remove('\t');
}

inline void HttpServer::replaceTag(QString &page, const QString &tag, const QString &payload)
{
    page.replace("{{"+tag+"}}", payload);
}

void HttpServer::writeRegularPage(QTcpSocket *socket, QString &urlPath, bool isHeadRequest)
{
    auto renderStart = QDateTime::currentMSecsSinceEpoch();
    QString searchRequest;
    bool isRegexp {false};
    int specSymbol = urlPath.indexOf('?'); // any actions like a ?toSearch=
    if (specSymbol != -1) {
        searchRequest = global::getValue(urlPath, "toSearch", global::eForWeb);
        isRegexp = global::getValue(urlPath, "isRegexp", global::eForWeb) == "on";
        urlPath.remove(specSymbol, urlPath.size()-specSymbol);
    }

    QString server = getWordFromPath(urlPath);
    QDir fsPath(m_dataFolder+server);
    if (not fsPath.exists()) {
        if (isHeadRequest) {
            if (socket->isOpen()) socket->write(HEADER_404.toUtf8());
        } else {
            writeErrorPage(socket, "REQUESTED SERVER LOG NOT EXIST");
        }
        return;
    }

    urlPath.remove(QRegularExpression("^.*/"+server));
    QString channel = getWordFromPath(urlPath);
    channel.remove(QRegularExpression("\\?.*$"));
    if (channel.isEmpty()) {
        writeAboutServerPage(socket, server, isHeadRequest);
        return;
    }

    if (not fsPath.cd(channel)) {
        if (isHeadRequest) {
            if (socket->isOpen()) socket->write(HEADER_404.toUtf8());
        } else {
            writeErrorPage(socket, "REQUESTED CHANNEL LOG NOT EXIST");
        }
        return;
    }

    QString originalServerName;
    for (const auto &s: m_servers) {
        if (global::toLowerAndNoSpaces(s.first) == server) {
            originalServerName = s.first;
        }
    }

    QString originalChannelName;
    for (const auto &server: m_servers) {
        for (const auto &channel_users: server.second) {
            if (global::toLowerAndNoSpaces(channel_users.first) == "#"+channel) {
                originalChannelName = global::toLowerAndNoSpaces(channel_users.first);
            }
        }
    }

    urlPath.remove(QRegularExpression("^.*/"+channel));
    QString year = getWordFromPath(urlPath);
    year.remove(QRegularExpression("\\?.*$"));
    QString month;
    QString day;
    if (not year.isEmpty() and fsPath.cd(year)) {
        urlPath.remove(QRegularExpression("^.*/"+year));
        month = getWordFromPath(urlPath);
        month.remove(QRegularExpression("\\?.*$"));
        if (not month.isEmpty() and fsPath.cd(month)) {
            if (urlPath.startsWith("/"+month+"/")) {
                urlPath.remove(0,1);
                int pos = urlPath.indexOf('/');
                if (pos != -1) {
                    urlPath.remove(0,pos);
                    day = getWordFromPath(urlPath);

                    if (urlPath.endsWith(".txt")) {
                        QFile plain(fsPath.path()+global::slash+day);
                        if (plain.open(QIODevice::ReadOnly)) {
                            QString header = HEADER_TEXT;
                            QByteArray file = plain.readAll();
                            plain.close();
                            replaceTag(header, "SIZE", QString::number(file.size()));
                            if (socket->isOpen()) {
                                socket->write(header.toUtf8());
                                if (not isHeadRequest) socket->write(file);
                            }
                        }
                        else {
                            if (isHeadRequest) {
                                if (socket->isOpen()) socket->write(HEADER_404.toUtf8());
                            } else {
                                writeErrorPage(socket, "FILE OPENING FAILED");
                            }
                        }
                        return;
                    }
                    else {
                        if (not QFile::exists(fsPath.path()+global::slash+day+".txt")) {
                            day.clear();
                        }
                    }
                }
            }
        }
        else { month.clear(); }
    }
    else { year.clear(); }

    QFile main("://html/main.html");
    QString page;
    if (main.open(QIODevice::ReadOnly)) {
        page = main.readAll();
        main.close();
    }
    else {
        if (socket->isOpen()) {
            socket->write(HEADER_404.toUtf8());
            if (not isHeadRequest) socket->write(CRITICAL_ERROR);
        }
    }

    if (isRegexp) {
        page.replace("<input id=\"main_header__search_checkbox__button\" type=\"checkbox\" name=\"isRegexp\">",
                     "<input id=\"main_header__search_checkbox__button\" type=\"checkbox\" name=\"isRegexp\" checked>");
    }

    //// Left menu compilation
    QString htmlServersSectionS;
    for (const auto &s: m_servers) {
        if (s.first.isEmpty()) continue; // empty server name?

        QString htmlServersSection = HTML_SERVER_SECTION;
        replaceTag(htmlServersSection, "ABOUT_SERVER", "/"+s.first);
        replaceTag(htmlServersSection, "SERVER_NAME", s.first);

        QString htmlChannelLineS;
        for (const auto &c: s.second) {
            QString htmlChannelLine;
            if (originalServerName == s.first and originalChannelName == c.first) {
                htmlChannelLine = HTML_SERVER_SECTION_CHANNEL_SELECTED;
            } else {
                htmlChannelLine = HTML_SERVER_SECTION_CHANNEL;
            }
            replaceTag(htmlChannelLine, "CHANNEL_NAME", c.first);

            QString channelNameForUrl {c.first};
            channelNameForUrl.remove('#');
            QString channelLink = "/" + global::toLowerAndNoSpaces(s.first) + "/" + channelNameForUrl;
            if (not year.isEmpty()) {
                channelLink += "/" + year;
                if (not month.isEmpty()) {
                    channelLink += "/" + month;
                    if (not day.isEmpty()) {
                        channelLink += "/" + day;
                    }
                }
            }

            replaceTag(htmlChannelLine, "CHANNEL_LINK", channelLink);

            htmlChannelLineS += htmlChannelLine;
        }
        replaceTag(htmlServersSection, "CHANNELS", htmlChannelLineS);

        bool online {false};
        for (const auto &srv: m_serversOnline) {
            if (srv.first == s.first) {
                online = srv.second;
                break;
            }
        }
        if (online) {
            replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_ONLINE_MARKER);
        } else {
            replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_OFFLINE_MARKER);
        }
        if (s.first == originalServerName) {
            htmlServersSectionS.push_front(htmlServersSection);
        } else {
            htmlServersSectionS += htmlServersSection;
        }
    }
    htmlServersSectionS.push_front(m_serviceButton);
    replaceTag(page, "SERVERS_SECTION", htmlServersSectionS);

    //// Main section header compilation
    QString& topic = m_channelsTopic[originalServerName][originalChannelName];
    topic = topic.replace('\"', "&quot;");

    QString titlePostfix = " | " + m_serviceName;
    if (not topic.isEmpty()) {
        titlePostfix.push_front(" | " + topic);
    }
    if (m_servers.size() > 1) {
        replaceTag(page, "PAGE_TITLE", originalChannelName + " ("+originalServerName+")" + titlePostfix);
    } else {
        replaceTag(page, "PAGE_TITLE", originalChannelName + titlePostfix);
    }

    replaceTag(page, "CHANNEL_TOPIC", topic);
    replaceTag(page, "MAIN_HEADER", originalChannelName);

    if (m_ajaxIsDisabled) {
        page.remove("<a href=\"{{REALTIME_LINK}}\" title=\"{{AIRPLAIN_TITLE}}\" class=\"main_header__title_airplaine\"></a>");
    }
    else {
        replaceTag(page, "REALTIME_LINK", "/~realtime/"+server+"/"+channel);
        replaceTag(page, "AIRPLAIN_TITLE", "Read in real time");
    }


    QString middlePath = "<a style=\"text-decoration: none\" href=\"/"+server+"/"+channel+"\">" + "/" + "</a>";
    if (not year.isEmpty()) {
        middlePath += "<a style=\"text-decoration: none\" href=\"/"+server+"/"+channel+"/"+year+"\">" + year + "</a>";
        if (not month.isEmpty()) {
            middlePath += "/<a style=\"text-decoration: none\" href=\"/"+server+"/"+channel+"/"+year+"/"+month+"\">" + month + "</a>";
        }
    }

    QString arrows = HTML_PAYLOAD_ADDITIONAL_ARROWS;
    QString currentYear {QDateTime::currentDateTime().toString("yyyy")};
    QString currentMonth {QDateTime::currentDateTime().toString("MM")};
    QString currentDay {QDateTime::currentDateTime().toString("dd")};
    replaceTag(arrows, "CURRENT_DATA_LOG", "/"+server+"/"+channel+"/"+
                       currentYear+"/"+currentMonth+"/"+currentDay);
    replaceTag(page, "ADDITIONAL_BUTTON", arrows);

    int currentOnline = 0;
    QString onlineUserS;
    for (const auto &user: m_servers[originalServerName][originalChannelName]) {
        if (QRegularExpression("^(.*;|~|@|\\&|\\+)?"+m_botNick[originalServerName]+"$").match(user).hasMatch()) {
            continue;
        }
        QString onlineUser = HTML_ONLINE_POINT;
        replaceTag(onlineUser, "NICKNAME", user);
        onlineUserS += onlineUser;
        currentOnline++;
    }
    replaceTag(page, "ONLINE", QString::number(currentOnline));
    replaceTag(page, "ONLINE_LIST", onlineUserS);

    if (not searchRequest.isEmpty()) {
        page.replace("<input class=\"main_header__search_input\" type=\"search\" name=\"toSearch\" placeholder=\"{{SEARCH_PLACEHOLDER}}\">",
                     "<input class=\"main_header__search_input\" type=\"search\" name=\"toSearch\" value=\"" + searchRequest.toHtmlEscaped() + "\">");
    } else if (middlePath == "<a style=\"text-decoration: none\" href=\"/"+server+"/"+channel+"\">" + "/" + "</a>") {
        replaceTag(page, "SEARCH_PLACEHOLDER", originalChannelName);
    } else if (month.isEmpty()) {
        replaceTag(page, "SEARCH_PLACEHOLDER", originalChannelName + "/" + year);
    } else {
        replaceTag(page, "SEARCH_PLACEHOLDER", originalChannelName + "/" + year + "/" + month);
    }

    //// Main section body compilation
    QString payloadBlock;

    std::pair<QString,QString> lastGreenNickname; // nick, color
    std::pair<QString,QString> lastRedNickname; // nick, color
    quint64 messageAnchorCounter = 0;
    NickColorist nickColorist;
    bool colorEdited = false;

    // Search request
    if (not searchRequest.isEmpty()) {
        static QMutex searchMtx;

        uint counter = 0;
        QRegularExpression userRgx(searchRequest, QRegularExpression::CaseInsensitiveOption);
        bool rgxIsValid = false;
        if (isRegexp and userRgx.isValid()) {
            rgxIsValid = true;
        }

        if (searchMtx.tryLock())
        {
            consoleLog("Search request (" + server + "): " + searchRequest);

            QStringList paths;
            QDirIterator it(fsPath.path());
            while (it.hasNext()) {
                QString currentPath = it.next();
                if (currentPath.endsWith(".") or currentPath.endsWith("..")) continue;
                QString logFolder = m_dataFolder;
    #ifdef WIN32
                logFolder.replace('\\', '/');
    #endif

                QString server {currentPath}; // Folder wich is not server folder is ignored
                server.remove(QRegularExpression("^"+logFolder));
                server.remove(QRegularExpression("/.*$"));

                bool serverIsOk = false;
                for (const auto &srv: m_servers) {
                    if (global::toLowerAndNoSpaces(srv.first) == server) {
                        serverIsOk = true;
                        break;
                    }
                }
                if (not serverIsOk) continue;

                QString currentChannel {currentPath}; // Folder wich is not channel folder is ignored
                currentChannel.remove(QRegularExpression("^"+logFolder+"[^/]*/"));
                currentChannel.remove(QRegularExpression("/.*$"));

                bool channelIsOk = false; // Канал явно указан в конфиге
                for (const auto &ch: m_servers[originalServerName]) {
                    QString searchChan {ch.first};
                    searchChan.remove('#');
                    if (searchChan == currentChannel) {
                        channelIsOk = true;
                        break;
                    }
                }
                if (not channelIsOk) continue;

                paths.push_back(currentPath);
            }

            if (paths.isEmpty()) {
                payloadBlock = HTML_PAYLOAD_ERROR;
                replaceTag(payloadBlock, "ERROR_TITLE", "Not found");
                replaceTag(payloadBlock, "ERROR_TEXT", "");
            }
            else {
                std::map<QString, QStringList> matchedPathsAndMessages;

                if (not month.isEmpty()) {
                    for (const auto& path: paths) {
                        if (not QRegularExpression("^.*[0-9]{2}\\.txt$").match(path).hasMatch()) continue;

                        QFile file(path);
                        if (not file.open(QIODevice::ReadOnly)) {
                            consoleLog("Error! I can't open log file " + fsPath.path());
                            continue;
                        }
                        QString buffer {file.readLine()};
                        while (not buffer.isEmpty()) {
                            removeBrakelineSymbols(buffer);
                            messageAnchorCounter++;

                            bool finded = false;
                            if (rgxIsValid) {
                                if (QRegularExpression(searchRequest, QRegularExpression::CaseInsensitiveOption).match(buffer).hasMatch()) {
                                    finded = true;
                                }
                            } else {
                                if (buffer.contains(searchRequest, Qt::CaseInsensitive)) {
                                    finded = true;
                                }
                            }
                            if (finded) {
                                std::pair<QString, QString> rawMessage = splitUserNameAndMessage(buffer);
                                if (rawMessage.first.isEmpty() or rawMessage.second.isEmpty()) {
                                    buffer = file.readLine();
                                    continue;
                                }
                                counter++;
                                QString message = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
                                if (rawMessage.second == global::BLINDED_MESSAGE_MERKER) {
                                    message.replace("class=\"main_payload__chat\"",
                                                    "class=\"main_payload__chat\" style=\"opacity: .5\"");
                                }
                                message.remove(" name=\"{{ANCHOR}}\"");
                                for (const auto &user: m_servers[originalServerName][originalChannelName]) {
                                    if (QRegularExpression("^(.*;|~|@|\\&|\\+)?"+rawMessage.first+"$").match(user).hasMatch()) {
                                        if (lastGreenNickname.first == rawMessage.first) {
                                            replaceTag(message, "COLOR", lastGreenNickname.second);
                                        }else{
                                            replaceTag(message, "COLOR", nickColorist.getGreenColor());
                                            lastGreenNickname.first = rawMessage.first;
                                            lastGreenNickname.second = nickColorist.getGreenColor(false);
                                        }
                                        break;
                                    }
                                }
                                if (not colorEdited) {
                                    if (lastRedNickname.first == rawMessage.first) {
                                        replaceTag(message, "COLOR", lastRedNickname.second);
                                    }else{
                                        replaceTag(message, "COLOR", nickColorist.getRedColor());
                                        lastRedNickname.first = rawMessage.first;
                                        lastRedNickname.second = nickColorist.getRedColor(false);
                                    }
                                }
                                QString logFolder {m_dataFolder};
                                logFolder.remove(QRegularExpression(".$"));
                                QString link {path};
                                link.remove(logFolder);
                                link.remove(QRegularExpression("\\.txt$"));
                                replaceTag(message, "ANCHOR", link+"#"+ANCHOR_SUFFIX+QString::number(messageAnchorCounter-1));
                                replaceTag(message, "USERNAME", rawMessage.first);
                                replaceTag(message, "MESSAGE_TEXT", rawMessage.second);
                                matchedPathsAndMessages[path].push_back(message);
                            }
                            buffer = file.readLine();
                        }
                        file.close();
                        messageAnchorCounter = 0;
                    }
                }
                else if (month.isEmpty() and not year.isEmpty()){
                    for (const auto &p: paths) {
                        QStringList slavePaths;

                        QDirIterator it(p);
                        while (it.hasNext()) {
                            QString fileName = it.next();
                            if (fileName.endsWith(".") or fileName.endsWith("..")) continue;
                            if (not QRegularExpression("\\.txt$").match(fileName).hasMatch()) continue;
                            slavePaths.push_back(fileName);
                        }
                        for (const auto &path: slavePaths) {
                            if (not QRegularExpression("^.*[0-9]{2}\\.txt$").match(path).hasMatch()) continue;

                            QFile file(path);
                            if (not file.open(QIODevice::ReadOnly)) {
                                consoleLog("Error! I can't open log file " + fsPath.path());
                                continue;
                            }
                            QString buffer {file.readLine()};
                            while (not buffer.isEmpty()) {
                                messageAnchorCounter++;
                                removeBrakelineSymbols(buffer);

                                bool finded = false;
                                if (rgxIsValid) {
                                    if (QRegularExpression(searchRequest, QRegularExpression::CaseInsensitiveOption).match(buffer).hasMatch()) {
                                        finded = true;
                                    }
                                } else {
                                    if (buffer.contains(searchRequest, Qt::CaseInsensitive)) {
                                        finded = true;
                                    }
                                }
                                if (finded) {
                                    std::pair<QString, QString> rawMessage = splitUserNameAndMessage(buffer);
                                    if (rawMessage.first.isEmpty() or rawMessage.second.isEmpty()) {
                                        buffer = file.readLine();
                                        continue;
                                    }
                                    counter++;
                                    QString message = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
                                    if (rawMessage.second == global::BLINDED_MESSAGE_MERKER) {
                                        message.replace("class=\"main_payload__chat\"",
                                                        "class=\"main_payload__chat\" style=\"opacity: .5\"");
                                    }
                                    message.remove(" name=\"{{ANCHOR}}\"");
                                    for (const auto &user: m_servers[originalServerName][originalChannelName]) {
                                        if (QRegularExpression("^(.*;|~|@|\\&|\\+)?"+rawMessage.first+"$").match(user).hasMatch()) {
                                            if (lastGreenNickname.first == rawMessage.first) {
                                                replaceTag(message, "COLOR", lastGreenNickname.second);
                                            }else{
                                                replaceTag(message, "COLOR", nickColorist.getGreenColor());
                                                lastGreenNickname.first = rawMessage.first;
                                                lastGreenNickname.second = nickColorist.getGreenColor(false);
                                            }
                                            break;
                                        }
                                    }
                                    if (not colorEdited) {
                                        if (lastRedNickname.first == rawMessage.first) {
                                            replaceTag(message, "COLOR", lastRedNickname.second);
                                        }else{
                                            replaceTag(message, "COLOR", nickColorist.getRedColor());
                                            lastRedNickname.first = rawMessage.first;
                                            lastRedNickname.second = nickColorist.getRedColor(false);
                                        }
                                    }
                                    QString logFolder {m_dataFolder};
                                    logFolder.remove(QRegularExpression(".$"));
                                    QString link {path};
                                    link.remove(logFolder);
                                    link.remove(QRegularExpression("\\.txt$"));
                                    replaceTag(message, "ANCHOR", link+"#"+ANCHOR_SUFFIX+QString::number(messageAnchorCounter-1));
                                    replaceTag(message, "USERNAME", rawMessage.first);
                                    replaceTag(message, "MESSAGE_TEXT", rawMessage.second);
                                    matchedPathsAndMessages[path].push_back(message);
                                }
                                buffer = file.readLine();
                            }
                            file.close();
                            messageAnchorCounter = 0;
                        }
                    }
                }
                else { // root directory
                    QStringList yearPaths;
                    for (const auto& p: paths) {
                        if (not QRegularExpression("/2[0-9]{3}$").match(p).hasMatch()) continue;
                        yearPaths.push_back(p);
                    }
                    QStringList fileNameS;
                    for (const auto& p: yearPaths) {
                        QStringList slavePaths;

                        QDirIterator it(p);
                        while (it.hasNext()) {
                            QString folderName = it.next();
                            if (folderName.endsWith(".") or folderName.endsWith("..")) continue;
                            if (not QRegularExpression("/[0-9]{2}$").match(folderName).hasMatch()) continue;
                            slavePaths.push_back(folderName);
                        }

                        for (const auto &path: slavePaths) {
                            QDirIterator itMonth(path);
                            while (itMonth.hasNext()) {
                                QString fileName = itMonth.next();
                                if (fileName.endsWith(".") or fileName.endsWith("..")) continue;
                                if (not QRegularExpression("^.*[0-9]{2}\\.txt$").match(fileName).hasMatch()) continue;
                                fileNameS.push_back(fileName);
                            }
                        }
                    }

                    for (const auto& path: fileNameS) {
                        QFile file(path);
                        if (not file.open(QIODevice::ReadOnly)) {
                            consoleLog("Error! I can't open log file " + fsPath.path());
                            continue;
                        }
                        QString buffer {file.readLine()};
                        while (not buffer.isEmpty()) {
                            removeBrakelineSymbols(buffer);
                            messageAnchorCounter++;

                            bool finded = false;
                            if (rgxIsValid) {
                                if (QRegularExpression(searchRequest, QRegularExpression::CaseInsensitiveOption).match(buffer).hasMatch()) {
                                    finded = true;
                                }
                            } else {
                                if (buffer.contains(searchRequest, Qt::CaseInsensitive)) {
                                    finded = true;
                                }
                            }
                            if (finded) {
                                std::pair<QString, QString> rawMessage = splitUserNameAndMessage(buffer);
                                if (rawMessage.first.isEmpty() or rawMessage.second.isEmpty()) {
                                    buffer = file.readLine();
                                    continue;
                                }
                                counter++;
                                QString message = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
                                if (rawMessage.second == global::BLINDED_MESSAGE_MERKER) {
                                    message.replace("class=\"main_payload__chat\"",
                                                    "class=\"main_payload__chat\" style=\"opacity: .5\"");
                                }
                                message.remove(" name=\"{{ANCHOR}}\"");
                                for (const auto &user: m_servers[originalServerName][originalChannelName]) {
                                    if (QRegularExpression("^(.*;|~|@|\\&|\\+)?"+rawMessage.first+"$").match(user).hasMatch()) {
                                        if (lastGreenNickname.first == rawMessage.first) {
                                            replaceTag(message, "COLOR", lastGreenNickname.second);
                                        }else{
                                            replaceTag(message, "COLOR", nickColorist.getGreenColor());
                                            lastGreenNickname.first = rawMessage.first;
                                            lastGreenNickname.second = nickColorist.getGreenColor(false);
                                        }
                                        break;
                                    }
                                }
                                if (not colorEdited) {
                                    if (lastRedNickname.first == rawMessage.first) {
                                        replaceTag(message, "COLOR", lastRedNickname.second);
                                    }else{
                                        replaceTag(message, "COLOR", nickColorist.getRedColor());
                                        lastRedNickname.first = rawMessage.first;
                                        lastRedNickname.second = nickColorist.getRedColor(false);
                                    }
                                }
                                QString logFolder {m_dataFolder};
                                logFolder.remove(QRegularExpression(".$"));
                                QString link {path};
                                link.remove(logFolder);
                                link.remove(QRegularExpression("\\.txt$"));
                                replaceTag(message, "ANCHOR", link+"#"+ANCHOR_SUFFIX+QString::number(messageAnchorCounter-1));
                                replaceTag(message, "USERNAME", rawMessage.first);
                                replaceTag(message, "MESSAGE_TEXT", rawMessage.second);
                                matchedPathsAndMessages[path].push_back(message);
                            }
                            buffer = file.readLine();
                        }
                        file.close();
                        messageAnchorCounter = 0;
                    }
                }

                if (matchedPathsAndMessages.empty()) {
                    payloadBlock = HTML_PAYLOAD_ERROR;
                    replaceTag(payloadBlock, "ERROR_TITLE", "Not found");
                    replaceTag(payloadBlock, "ERROR_TEXT", "");
                }
                else {
                    QStringList findedPaths;
                    for (const auto& fp: matchedPathsAndMessages) {
                        findedPaths << fp.first;
                    }
                    std::sort(findedPaths.begin(), findedPaths.end());

                    for (auto& link: findedPaths) {
                        QString logFolder {m_dataFolder};
                        logFolder.remove(QRegularExpression(".$"));
                        QStringList& messages = matchedPathsAndMessages[link];
                        link.remove(logFolder);
                        link.remove(QRegularExpression("\\.txt$"));

                        QString finded = HTML_PAYLOAD_LIST_POINT_MESSAGE;
                        finded.replace("class=\"main_payload__block\"", "class=\"main_payload__block\" style=\"background: #b6c7d6\"");
                        replaceTag(finded, "POINT_LINK", link);
                        link.remove(QRegularExpression("^.*"+channel));
                        replaceTag(finded, "POINT_CONTENT", link);
                        payloadBlock += finded;
                        for(const auto& m: messages) {
                            payloadBlock += m;
                        }
                        payloadBlock += "&nbsp;\n";
                    }
                }
            }
            int lastWbr = 0;
            for (int i = 0; i < searchRequest.size(); i++) {
                if (i-lastWbr > MAX_MESSAGE_LENGTH_WITHOUT_WBR) {
                    searchRequest.insert(i, "<wbr>");
                    lastWbr = i;
                }
            }
            searchMtx.unlock();
        } // searchMtx.tryLock()
        else
        {
            consoleLog("Search request (" + server + "): " + searchRequest + " rejected by mutex");
            payloadBlock = HTML_PAYLOAD_ERROR;
            replaceTag(payloadBlock, "ERROR_TITLE", "Try later");
            replaceTag(payloadBlock, "ERROR_TEXT", "Please try again later. The search service is busy.");
        }
        middlePath += "&nbsp;<span style=\"color: #4f9c65\">" + searchRequest.toHtmlEscaped() + "</span>&nbsp;";
        if (rgxIsValid) middlePath += "rgx";
        middlePath += "(" + QString::number(counter) + ")";
    }

    // Plain log explorer
    else {

        if (year.isEmpty()) { // /
            QStringList folderNameS;
            QDirIterator it(fsPath.path());
            while (it.hasNext()) {
                QString folderName = it.next();
                if (folderName.endsWith(".") or folderName.endsWith("..")) continue;
                while(folderName.contains('/')) {
                    folderName.remove(QRegularExpression("^.*/"));
                }
                folderName.remove(QRegularExpression("\\.txt$"));
                folderNameS << folderName;
            }
            if (not folderNameS.isEmpty()) {
                std::sort(folderNameS.begin(), folderNameS.end());
                for (const auto &f: folderNameS) {
                    QString onePoint = HTML_PAYLOAD_LIST_POINT_FOLDER;
                    replaceTag(onePoint, "POINT_CONTENT", f);
                    replaceTag(onePoint, "POINT_LINK", "/"+server+"/"+channel+"/"+f);
                    payloadBlock += onePoint;
                }
            }
        }
        else if (not year.isEmpty() and month.isEmpty()) { // /YYYY
            QStringList folderNameS;
            QDirIterator it(fsPath.path());
            while (it.hasNext()) {
                QString folderName = it.next();
                if (folderName.endsWith(".") or folderName.endsWith("..")) continue;
                while(folderName.contains('/')) {
                    folderName.remove(QRegularExpression("^.*/"));
                }
                folderNameS << folderName;
            }
            if (not folderNameS.isEmpty()) {
                std::sort(folderNameS.begin(), folderNameS.end());
                for (const auto &f: folderNameS) {
                    QString onePoint = HTML_PAYLOAD_LIST_POINT_FOLDER;
                    bool yearIsOk {false};
                    bool monthIsOk {false};
                    QDate dateOfMonth (year.toInt(&yearIsOk), f.toInt(&monthIsOk), 1);
                    QString nameOfMonth;
                    if (yearIsOk and monthIsOk) {
                        nameOfMonth = QLocale().standaloneMonthName(dateOfMonth.month(), QLocale::ShortFormat);
                    }
                    QDirIterator dayLogs(fsPath.path() + global::slash + f);
                    int8_t dayLogsCounter {0};
                    while (dayLogs.hasNext()) {
                        QString dayLogFile = dayLogs.next();
                        if (dayLogFile.endsWith(".") or dayLogFile.endsWith("..")) continue;
                        dayLogsCounter++;
                    }
                    QString filesLabel;
                    dayLogsCounter == 1 ? filesLabel = "day" : filesLabel = "days";
                    replaceTag(onePoint, "POINT_CONTENT", "<span style=\"font-weight: bold\">" + f + "</span>&nbsp;(" + nameOfMonth + ") " + QString::number(dayLogsCounter) + " " + filesLabel);
                    replaceTag(onePoint, "POINT_LINK", "/"+server+"/"+channel+"/"+year+"/"+f);
                    payloadBlock += onePoint;
                }
            }
        }
        else if (not month.isEmpty() and day.isEmpty()) { // /YYYY/MM
            QStringList fileNameS;
            QDirIterator it(fsPath.path());
            while (it.hasNext()) {
                QString fileName = it.next();
                if (fileName.endsWith(".") or fileName.endsWith("..")) continue; // QDir::NoDotAndNoDotDot not works!
                while(fileName.contains('/')) {
                    fileName.remove(QRegularExpression("^.*/"));
                }
                fileName.remove(QRegularExpression("\\.txt$"));
                fileNameS << fileName;
            }
            if (not fileNameS.isEmpty()) {
                std::sort(fileNameS.begin(), fileNameS.end());
                for (const auto &a: fileNameS) {
                    QString onePoint = HTML_PAYLOAD_LIST_POINT_MESSAGE;
                    bool yearIsOk {false};
                    bool monthIsOk {false};
                    bool dayIsOk {false};
                    QDate dateOfDay (year.toInt(&yearIsOk), month.toInt(&monthIsOk), a.toInt(&dayIsOk));
                    QString nameOfDay;
                    if (yearIsOk and monthIsOk and dayIsOk) {
                        nameOfDay = QLocale().standaloneDayName(dateOfDay.dayOfWeek(), QLocale::ShortFormat);
                    }
                    auto logFileSizeBytes = QFile(fsPath.path() + global::slash + a +".txt").size();
                    auto logFileSizeinKb = logFileSizeBytes/1000;
                    QString logFileSizeString;
                    if (logFileSizeinKb != 0) {
                        logFileSizeString = QString::number(logFileSizeinKb)+"."+
                                QString::number((logFileSizeBytes - logFileSizeinKb*1000)/10)+
                                " KB";
                    } else {
                        logFileSizeString = QString::number(logFileSizeBytes)+" B";
                    }
                    replaceTag(onePoint, "POINT_CONTENT", "<span style=\"font-weight: bold\">" + a + "</span>&nbsp;(" + nameOfDay + ") " + logFileSizeString);
                    replaceTag(onePoint, "POINT_LINK", "/"+server+"/"+channel+"/"+year+"/"+month+"/"+a);
                    payloadBlock += onePoint;
                }
            }
        }
        else if (not day.isEmpty()) { // /YYYY/MM/dd
            QFile file(fsPath.path()+global::slash+day+".txt");
            if (not file.open(QIODevice::ReadOnly)) {
                consoleLog("Error! I can't open log file " + fsPath.path());
                payloadBlock = HTML_PAYLOAD_ERROR;
                replaceTag(payloadBlock, "ERROR_TITLE", "Internal error");
                replaceTag(payloadBlock, "ERROR_TEXT", "Requested log file openning failed");
            }
            else {
                middlePath += "/" + day;
                QString buffer = file.readLine();
                while (not buffer.isEmpty()) {
                    removeBrakelineSymbols(buffer);

                    std::pair<QString, QString> rawMessage = splitUserNameAndMessage(buffer);
                    if (rawMessage.first.isEmpty() or rawMessage.second.isEmpty()) {
                        buffer = file.readLine();
                        continue;
                    }

                    QString message = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
                    if (rawMessage.second == global::BLINDED_MESSAGE_MERKER) {
                        message.replace("class=\"main_payload__chat\"",
                                        "class=\"main_payload__chat\" style=\"opacity: .5\"");
                    }
                    for (const auto &user: m_servers[originalServerName][originalChannelName]) {
                        if (QRegularExpression("^(.*;|~|@|\\&|\\+)?"+rawMessage.first+"$").match(user).hasMatch()) {
                            if (lastGreenNickname.first == rawMessage.first) {
                                replaceTag(message, "COLOR", lastGreenNickname.second);
                            }else{
                                replaceTag(message, "COLOR", nickColorist.getGreenColor());
                                lastGreenNickname.first = rawMessage.first;
                                lastGreenNickname.second = nickColorist.getGreenColor(false);
                            }
                            break;
                        }
                    }
                    if (not colorEdited) {
                        if (lastRedNickname.first == rawMessage.first) {
                            replaceTag(message, "COLOR", lastRedNickname.second);
                        }else{
                            replaceTag(message, "COLOR", nickColorist.getRedColor());
                            lastRedNickname.first = rawMessage.first;
                            lastRedNickname.second = nickColorist.getRedColor(false);
                        }
                    }
                    message.replace("href=\"", "href=\"#");
                    replaceTag(message, "ANCHOR", ANCHOR_SUFFIX+QString::number(messageAnchorCounter++));
                    replaceTag(message, "USERNAME", rawMessage.first);
                    replaceTag(message, "MESSAGE_TEXT", rawMessage.second);
                    payloadBlock += message;
                    buffer = file.readLine();
                }
                file.close();

                // arrows to prev & next days
                bool prevDayFound {false};
                uint prevDayUInt {day.toUShort()};
                uint prevMonthUInt {month.toUShort()};
                uint prevYearUInt {year.toUShort()};
                QDir fsPathForPrev {fsPath};
                for(uint i = prevDayUInt-1; i >= 1; i--) { // current month
                    QString d {fsPathForPrev.path()+global::slash};
                    if (i < 10) d += "0";
                    d += QString::number(i)+".txt";
                    if (QFile::exists(d)) {
                        prevDayUInt = i;
                        prevDayFound = true;
                        break;
                    }
                }
                if (not prevDayFound) { // other month
                    fsPathForPrev.cdUp();
                    bool mf {false};
                    for (uint i = prevMonthUInt-1; i >= 1; i--) {
                        QString m;
                        if (i < 10) m += "0";
                        m += QString::number(i);
                        if (fsPathForPrev.cd(m)) {
                            prevMonthUInt = i;
                            mf = true;
                            break;
                        }
                    }
                    if (mf) {
                        for(uint i = 31; i >= 1; i--) {
                            QString d {fsPathForPrev.path()+global::slash};
                            if (i < 10) d += "0";
                            d += QString::number(i)+".txt";
                            if (QFile::exists(d)) {
                                prevDayUInt = i;
                                prevDayFound = true;
                                break;
                            }
                        }
                    }
                }
                if (not prevDayFound) { // other year
                    fsPathForPrev.cdUp();
                    bool yf {false};
                    for (uint i = prevYearUInt-1; i > prevYearUInt-21; i--) {
                        if (fsPathForPrev.cd(QString::number(i))) {
                            prevYearUInt = i;
                            yf = true;
                            break;
                        }
                    }
                    if (yf) {
                        bool mf {false};
                        for (uint i = 12; i >= 1; i--) {
                            QString m;
                            if (i < 10) m += "0";
                            m += QString::number(i);
                            if (fsPathForPrev.cd(m)) {
                                prevMonthUInt = i;
                                mf = true;
                                break;
                            }
                        }
                        if (mf) {
                            for(uint i = 31; i >= 1; i--) {
                                QString d {fsPathForPrev.path()+global::slash};
                                if (i < 10) d += "0";
                                d += QString::number(i)+".txt";
                                if (QFile::exists(d)) {
                                    prevDayUInt = i;
                                    prevDayFound = true;
                                    break;
                                }
                            }
                        }
                    }
                }
                QString leftArrow;
                if (prevDayFound) {
                    leftArrow = HTML_PAYLOAD_MIDDLEPATH_ARROW_PREV;
                    QString link = "/"+server+"/"+channel+"/"+QString::number(prevYearUInt)+"/";
                    QString m;
                    if (prevMonthUInt < 10) m += "0";
                    m += QString::number(prevMonthUInt);
                    link += m+"/";
                    QString d;
                    if (prevDayUInt < 10) d += "0";
                    d += QString::number(prevDayUInt);
                    link += d;
                    replaceTag(leftArrow, "LINK", link);
                } else {
                    leftArrow = HTML_PAYLOAD_MIDDLEPATH_ARROW_PREV_FALSE;
                }
                middlePath += leftArrow;

                bool nextDayFound {false};
                uint nextDayUInt {day.toUShort()};
                uint nextMonthUInt {month.toUShort()};
                uint nextYearUInt {year.toUShort()};
                QDir fsPathForNext {fsPath};
                for(uint i = nextDayUInt+1; i <= 31; i++) { // current month
                    QString d {fsPathForNext.path()+global::slash};
                    if (i < 10) d += "0";
                    d += QString::number(i)+".txt";
                    if (QFile::exists(d)) {
                        nextDayUInt = i;
                        nextDayFound = true;
                        break;
                    }
                }
                if (not nextDayFound) { // other month
                    fsPathForNext.cdUp();
                    bool mf {false};
                    for (uint i = nextMonthUInt+1; i <= 12; i++) {
                        QString m;
                        if (i < 10) m += "0";
                        m += QString::number(i);
                        if (fsPathForNext.cd(m)) {
                            nextMonthUInt = i;
                            mf = true;
                            break;
                        }
                    }
                    if (mf) {
                        for(uint i = 1; i <= 31; i++) {
                            QString d {fsPathForNext.path()+global::slash};
                            if (i < 10) d += "0";
                            d += QString::number(i)+".txt";
                            if (QFile::exists(d)) {
                                nextDayUInt = i;
                                nextDayFound = true;
                                break;
                            }
                        }
                    }
                }
                if (not nextDayFound) { // other year
                    fsPathForNext.cdUp();
                    bool yf {false};
                    for (uint i = nextYearUInt+1; i < nextYearUInt+20; i++) {
                        if (fsPathForNext.cd(QString::number(i))) {
                            nextYearUInt = i;
                            yf = true;
                            break;
                        }
                    }
                    if (yf) {
                        bool mf {false};
                        for (uint i = 1; i <= 12; i++) {
                            QString m;
                            if (i < 10) m += "0";
                            m += QString::number(i);
                            if (fsPathForNext.cd(m)) {
                                nextMonthUInt = i;
                                mf = true;
                                break;
                            }
                        }
                        if (mf) {
                            for(uint i = 1; i <= 31; i++) {
                                QString d {fsPathForNext.path()+global::slash};
                                if (i < 10) d += "0";
                                d += QString::number(i)+".txt";
                                if (QFile::exists(d)) {
                                    nextDayUInt = i;
                                    nextDayFound = true;
                                    break;
                                }
                            }
                        }
                    }
                }
                QString rightArrow;
                if (nextDayFound) {
                    rightArrow = HTML_PAYLOAD_MIDDLEPATH_ARROW_NEXT;
                    QString link = "/"+server+"/"+channel+"/"+QString::number(nextYearUInt)+"/";
                    QString m;
                    if (nextMonthUInt < 10) m += "0";
                    m += QString::number(nextMonthUInt);
                    link += m+"/";
                    QString d;
                    if (nextDayUInt < 10) d += "0";
                    d += QString::number(nextDayUInt);
                    link += d;
                    replaceTag(rightArrow, "LINK", link);
                } else {
                    rightArrow = HTML_PAYLOAD_MIDDLEPATH_ARROW_NEXT_FALSE;
                }
                middlePath += rightArrow;
            }
        }
        if (payloadBlock.isEmpty()) {
            payloadBlock = HTML_PAYLOAD_ERROR;
            replaceTag(payloadBlock, "ERROR_TITLE", "Empty");
            replaceTag(payloadBlock, "ERROR_TEXT", "No logs found for this channel");
        }
    }

    replaceTag(page, "MIDDLE_PATH", middlePath);
    replaceTag(page, "PAYLOAD_BLOCK", payloadBlock);

    //// Footer
    replaceTag(page, "VERSION", global::IRCABOT_VERSION);
    replaceTag(page, "COPYRIGHT_YEAR", global::COPYRIGHT_YEAR);

    replaceTag(page, "RENDERING_TIMER", QString::number(QDateTime::currentMSecsSinceEpoch() - renderStart));

    QString mainHeader = HEADER_HTML;
    replaceTag(mainHeader, "SIZE", QString::number(QByteArray(page.toUtf8()).size()));

    if (socket->isOpen()) {
        socket->write(mainHeader.toUtf8());
        if (not isHeadRequest) socket->write(page.toUtf8());
    }
}

void HttpServer::writeRealTimeChatPage(QTcpSocket *socket, QString &urlPath, bool isHeadRequest)
{
    if (isHeadRequest) {
        QString header = HEADER_HTML;
        replaceTag(header, "SIZE", "0");
        socket->write(header.toUtf8());
        return;
    }

    auto renderStart = QDateTime::currentMSecsSinceEpoch();

    urlPath.remove(QRegularExpression("^/~realtime/"));
    QString server = getWordFromPath(urlPath);
    if (server.isEmpty()) {
        writeErrorPage(socket, "SERVER VALUE IS MISSING");
        return;
    }

    urlPath.remove(QRegularExpression("^"+server));
    QString channel = getWordFromPath(urlPath);
    if (channel.isEmpty()) {
        writeErrorPage(socket, "CHANNEL VALUE IS MISSING");
        return;
    }

    QString originalServerName;
    for (const auto &s: m_servers) {
        if (global::toLowerAndNoSpaces(s.first) == server) {
            originalServerName = s.first;
        }
    }
    if (originalServerName.isEmpty()) {
         writeErrorPage(socket, "REQUESTED SERVER NOT EXIST");
         return;
    }

    QString originalChannelName;
    for (const auto &server: m_servers) {
        for (const auto &channel_users: server.second) {
            if (global::toLowerAndNoSpaces(channel_users.first) == "#"+channel) {
                originalChannelName = global::toLowerAndNoSpaces(channel_users.first);
            }
        }
    }
    if (originalChannelName.isEmpty()) {
         writeErrorPage(socket, "REQUESTED CHANNEL NOT EXIST");
         return;
    }

    QFile main("://html/main.html");
    QString page;
    if (main.open(QIODevice::ReadOnly)) {
        page = main.readAll();
        main.close();
    }
    else {
        if (socket->isOpen()) {
            socket->write(HEADER_404.toUtf8());
            if (not isHeadRequest) socket->write(CRITICAL_ERROR);
        }
    }

    QString year {QDateTime::currentDateTime().toString("yyyy")};
    QString month {QDateTime::currentDateTime().toString("MM")};
    QString day {QDateTime::currentDateTime().toString("dd")};

    //// Left menu compilation
    QString htmlServersSectionS;
    for (const auto &s: m_servers) {
        if (s.first.isEmpty()) continue; // empty server name?

        QString htmlServersSection = HTML_SERVER_SECTION;
        replaceTag(htmlServersSection, "ABOUT_SERVER", "/"+server);
        replaceTag(htmlServersSection, "SERVER_NAME", s.first);
        if (s.first == originalServerName) {
            htmlServersSection.replace("<span style=\"font-size: 17px;\">{{ONLINE_STATUS}}",
                                       "<span style=\"font-size: 17px;\" id=\"serverStatus\">{{ONLINE_STATUS}}");
        }

        QString htmlChannelLineS;
        for (const auto &c: s.second) {
            QString htmlChannelLine;
            if (originalServerName == s.first and originalChannelName == c.first) {
                htmlChannelLine = HTML_SERVER_SECTION_CHANNEL_SELECTED;
            } else {
                htmlChannelLine = HTML_SERVER_SECTION_CHANNEL;
            }
            replaceTag(htmlChannelLine, "CHANNEL_NAME", c.first);

            QString channelNameForUrl {c.first};
            channelNameForUrl.remove('#');
            QString channelLink = "/~realtime/" + global::toLowerAndNoSpaces(s.first) + "/" + channelNameForUrl;
            replaceTag(htmlChannelLine, "CHANNEL_LINK", channelLink);

            htmlChannelLineS += htmlChannelLine;
        }
        replaceTag(htmlServersSection, "CHANNELS", htmlChannelLineS);

        bool online {false};
        for (const auto &srv: m_serversOnline) {
            if (srv.first == s.first) {
                online = srv.second;
                break;
            }
        }
        if (online) {
            replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_ONLINE_MARKER);
        } else {
            replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_OFFLINE_MARKER);
        }
        if (s.first == originalServerName) {
            htmlServersSectionS.push_front(htmlServersSection);
        } else {
            htmlServersSectionS += htmlServersSection;
        }
    }
    htmlServersSectionS.push_front(m_serviceButton);
    replaceTag(page, "SERVERS_SECTION", htmlServersSectionS);

    //// Main section header compilation
    QString& topic = m_channelsTopic[originalServerName][originalChannelName];
    topic = topic.replace('\"', "&quot;");

    QString titlePostfix = " | " + m_serviceName;
    if (not topic.isEmpty()) {
        titlePostfix.push_front(" | " + topic);
    }
    if (m_servers.size() > 1) {
        replaceTag(page, "PAGE_TITLE", originalChannelName + " ("+originalServerName+") [real time]" + titlePostfix);
    } else {
        replaceTag(page, "PAGE_TITLE", originalChannelName + " [real time]" + titlePostfix);
    }

    replaceTag(page, "CHANNEL_TOPIC", topic);
    replaceTag(page, "MAIN_HEADER", originalChannelName);
    replaceTag(page, "ADDITIONAL_BUTTON", HTML_PAYLOAD_ADDITIONAL_NOTIFY);
    replaceTag(page, "REALTIME_LINK", "/"+server+"/"+channel+"/"+year+"/"+month+"/"+day);
    replaceTag(page, "AIRPLAIN_TITLE", "Back to plain text log");
    page.replace("class=\"main_header__title_airplaine\"", "class=\"main_header__title_airplaine\" style=\"transform: scale(-1,1)\"");

    int currentOnline = 0;
    QString onlineUserS;
    for (const auto &user: m_servers[originalServerName][originalChannelName]) {
        if (QRegularExpression("^(.*;|~|@|\\&|\\+)?"+m_botNick[originalServerName]+"$").match(user).hasMatch()) {
            continue;
        }
        QString onlineUser = HTML_ONLINE_POINT;
        replaceTag(onlineUser, "NICKNAME", user);
        onlineUserS += onlineUser;
        currentOnline++;
    }
    page.replace("{{ONLINE}}", "<span id=\"online\">{{ONLINE}}</span>");
    replaceTag(page, "ONLINE", QString::number(currentOnline));
    page.replace("<div class=\"main_middle__online_list\">", "<div id=\"onlineList\" class=\"main_middle__online_list\">");
    replaceTag(page, "ONLINE_LIST", onlineUserS);

    page.replace("class=\"main_header__search_form\"", "action=\"/"+server+"/"+channel+"\" class=\"main_header__search_form\"");
    replaceTag(page, "SEARCH_PLACEHOLDER", originalChannelName);

    page.replace("<div class=\"main_middle__path\">", "<div class=\"main_middle__path\" id=\"path\">");
    replaceTag(page, "MIDDLE_PATH", "");

    //// Payload
    page.replace("<div class=\"main_payload\">", "<div class=\"main_payload\" id=\"payload\">");
    bool logsExisted {false};
    QFile file;
    QDir fsPath(m_dataFolder+server+global::slash+channel);
    if (fsPath.cd(year)) {
        if (fsPath.cd(month)) {
            file.setFileName(fsPath.path()+global::slash+day+".txt");
            if (file.open(QIODevice::ReadOnly)) {
                logsExisted = true;
            }
        }
    }

    QString payloadBlock;
    if (logsExisted) {
        QString buffer = file.readLine();
        while (not buffer.isEmpty()) {
            removeBrakelineSymbols(buffer);

            std::pair<QString, QString> rawMessage = splitUserNameAndMessage(buffer);
            if (rawMessage.first.isEmpty() or rawMessage.second.isEmpty()) {
                buffer = file.readLine();
                continue;
            }

            QString message = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
            message.remove("name=\"{{ANCHOR}}\" href=\"{{ANCHOR}}\" ");

            if (rawMessage.second == global::BLINDED_MESSAGE_MERKER) {
                message.replace("class=\"main_payload__chat\"",
                                "class=\"main_payload__chat\" style=\"opacity: .5\"");
            }
            message.replace("class=\"main_payload__chat_username\"",
                            "class=\"main_payload__chat_username\" style=\"color: #e34f10\"");

            replaceTag(message, "USERNAME", rawMessage.first);
            replaceTag(message, "MESSAGE_TEXT", rawMessage.second);
            payloadBlock += message;
            buffer = file.readLine();
        }
        file.close();
    }
    QString hr = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
    hr.replace("class=\"main_payload__chat_mail\"", "id=\"hr\" class=\"main_payload__chat_mail\"");
    hr.replace("class=\"main_payload__chat_username\"",
                    "class=\"main_payload__chat_username\" style=\"color: white; text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;\"");
    replaceTag(hr, "USERNAME", "IRCaBot");
    replaceTag(hr, "MESSAGE_TEXT", "<b>New messages won't show without JavaScript.</b><br>"
                                   "My JS code is small and simple. Check it at "
                                   "<a href=\"/realtimechat.js\">/realtimechat.js</a> and come back "
                                   "with enabled!");

    payloadBlock += hr;
    replaceTag(page, "PAYLOAD_BLOCK", payloadBlock);

    //// Footer
    replaceTag(page, "VERSION", global::IRCABOT_VERSION);
    replaceTag(page, "COPYRIGHT_YEAR", global::COPYRIGHT_YEAR);

    //// Finish
    page.replace("</body>", "    <div id=\"LMId\" style=\"display: none\">" + QString::number(QDateTime::currentMSecsSinceEpoch()) + "</div>\n"
                            "    <div id=\"ajaxPath\" style=\"display: none\">" + server + "/" + channel + "</div>\n"
                            "    <script src=\"/realtimechat.js\"></script>\n</body>");
    replaceTag(page, "RENDERING_TIMER", QString::number(QDateTime::currentMSecsSinceEpoch() - renderStart));
    QString mainHeader = HEADER_HTML;
    replaceTag(mainHeader, "SIZE", QString::number(QByteArray(page.toUtf8()).size()));

    if (socket->isOpen()) {
        socket->write(mainHeader.toUtf8());
        if (not isHeadRequest) socket->write(page.toUtf8());
    }
}

void HttpServer::writeAboutServerPage(QTcpSocket *socket, QString &server, bool isHeadRequest)
{
    auto renderStart = QDateTime::currentMSecsSinceEpoch();

    QString originalServerName;
    for (const auto &s: m_servers) {
        if (global::toLowerAndNoSpaces(s.first) == server) {
            originalServerName = s.first;
        }
    }
    if (originalServerName.isEmpty()) {
         writeErrorPage(socket, "SERVER NOT EXIST");
         return;
    }

    QFile main("://html/main.html");
    QString page;
    if (main.open(QIODevice::ReadOnly)) {
        page = main.readAll();
        main.close();
    }
    else {
        if (socket->isOpen()) {
            socket->write(HEADER_404.toUtf8());
            if (not isHeadRequest) socket->write(CRITICAL_ERROR);
        }
    }

    replaceTag(page, "PAGE_TITLE", "About " + originalServerName + " | " + m_serviceName);

    //// Left menu compilation
    QString htmlServersSectionS;
    for (const auto &s: m_servers) {
        if (s.first.isEmpty()) continue; // empty server name?

        QString htmlServersSection = HTML_SERVER_SECTION;
        if (s.first == originalServerName) {
            htmlServersSection.replace("<div class=\"left_menu__item\">",
                                       "<div class=\"left_menu__item\" style=\"background: #f0f5fa\">");
        }
        replaceTag(htmlServersSection, "ABOUT_SERVER", "/"+s.first);
        replaceTag(htmlServersSection, "SERVER_NAME", s.first);

        QString htmlChannelLineS;
        for (const auto &c: s.second) {
            QString htmlChannelLine;
            htmlChannelLine = HTML_SERVER_SECTION_CHANNEL;
            replaceTag(htmlChannelLine, "CHANNEL_NAME", c.first);

            QString channelNameForUrl {c.first};
            channelNameForUrl.remove('#');
            QString channelLink = "/" + global::toLowerAndNoSpaces(s.first) + "/" + channelNameForUrl;
            replaceTag(htmlChannelLine, "CHANNEL_LINK", channelLink);

            htmlChannelLineS += htmlChannelLine;
        }
        replaceTag(htmlServersSection, "CHANNELS", htmlChannelLineS);

        bool online {false};
        for (const auto &srv: m_serversOnline) {
            if (srv.first == s.first) {
                online = srv.second;
                break;
            }
        }
        if (online) {
            replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_ONLINE_MARKER);
        } else {
            replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_OFFLINE_MARKER);
        }
        if (s.first == originalServerName) {
            htmlServersSectionS.push_front(htmlServersSection);
        } else {
            htmlServersSectionS += htmlServersSection;
        }
    }
    htmlServersSectionS.push_front(m_serviceButton);
    replaceTag(page, "SERVERS_SECTION", htmlServersSectionS);
    page.remove(QRegularExpression("<div class=\"main_header\">.*<!-- main_middle -->", QRegularExpression::DotMatchesEverythingOption));

    QString payloadBlock = HTML_PAYLOAD_ABOUT;
    replaceTag(payloadBlock, "ABOUT_TITLE", originalServerName);

    QString aboutBlock;
    QFile about(m_dataFolder+server+global::slash+"about_server.txt");
    if (about.open(QIODevice::ReadOnly))
    {
        QString rbuffer = about.readLine();
        while (not rbuffer.isEmpty()) {
            if (rbuffer.startsWith('#')) {
                rbuffer = about.readLine();
                continue;
            }
            removeBrakelineSymbols(rbuffer);
            if (not rbuffer.isEmpty()) {
                aboutBlock += rbuffer;
            }
            rbuffer = about.readLine();
        }
    }
    else {
        aboutBlock = "No information provided";
    }
    replaceTag(payloadBlock, "ABOUT_TEXT", aboutBlock);

    replaceTag(page, "PAYLOAD_BLOCK", payloadBlock);

    //// Footer
    replaceTag(page, "VERSION", global::IRCABOT_VERSION);
    replaceTag(page, "COPYRIGHT_YEAR", global::COPYRIGHT_YEAR);

    //// Finish
    replaceTag(page, "RENDERING_TIMER", QString::number(QDateTime::currentMSecsSinceEpoch() - renderStart));
    QString mainHeader = HEADER_HTML;
    replaceTag(mainHeader, "SIZE", QString::number(QByteArray(page.toUtf8()).size()));

    if (socket->isOpen()) {
        socket->write(mainHeader.toUtf8());
        if (not isHeadRequest) socket->write(page.toUtf8());
    }
}

void HttpServer::writeAjaxAnswer(QTcpSocket *socket, QString &urlPath, bool isHeadRequest)
{
    if (isHeadRequest) {
        QString header = HEADER_JSON;
        replaceTag(header, "SIZE", "0");
        socket->write(header.toUtf8());
        return;
    }

    //// Validate
    urlPath.remove(QRegularExpression("^/ajax/"));
    QString server = getWordFromPath(urlPath);
    if (server.isEmpty()) {
        writeErrorJson(socket, "Invalid request. Server value is missing!");
        return;
    }

    urlPath.remove(QRegularExpression("^"+server));
    QString channel = getWordFromPath(urlPath);
    if (channel.isEmpty()) {
        writeErrorJson(socket, "Invalid request. Channel value is missing!");
        return;
    }

    QString originalServerName;
    for (const auto &s: m_servers) {
        if (global::toLowerAndNoSpaces(s.first) == server) {
            originalServerName = s.first;
        }
    }
    if (originalServerName.isEmpty()) {
         writeErrorJson(socket, "Invalid request. Server not exist!");
         return;
    }

    QString originalChannelName;
    for (const auto &server: m_servers) {
        for (const auto &channel_users: server.second) {
            if (global::toLowerAndNoSpaces(channel_users.first) == "#"+channel) {
                originalChannelName = global::toLowerAndNoSpaces(channel_users.first);
            }
        }
    }
    if (originalChannelName.isEmpty()) {
         writeErrorJson(socket, "Invalid request. Channel not exist!");
         return;
    }

    //// Parse
    bool userOnlineCounterIsOk {false};
    auto userOnlineCounter = global::getValue(urlPath, "onlineCounter", global::eForWeb).toInt(&userOnlineCounterIsOk);
    if (not userOnlineCounterIsOk) {
        writeErrorJson(socket, "Invalid request: 'onlineCounter' (int) value is missing!");
        return;
    }
    bool userMessageLastIdIsOk {false};
    qint64 userMessageLastId = global::getValue(urlPath, "messageId", global::eForWeb).toLongLong(&userMessageLastIdIsOk);
    if (not userMessageLastIdIsOk) {
        writeErrorJson(socket, "Invalid request: 'messageId' value is missing!");
        return;
    }
    bool userServerStatus = global::getValue(urlPath, "serverStatus", global::eForWeb) == "true";

    //// Building
    QJsonObject jResponse;
    jResponse.insert("status", QJsonValue(true));

    // online server
    if (userServerStatus != m_serversOnline[originalServerName]) {
        jResponse.insert("serverStatusChanged", QJsonValue(true));
        jResponse.insert("serverStatus", QJsonValue(m_serversOnline[originalServerName]));
    } else {
        jResponse.insert("serverStatusChanged", QJsonValue(false));
    }
    // online users
    if (m_servers[originalServerName][originalChannelName].size()-1/*self*/ != userOnlineCounter) {
        jResponse.insert("onlineUsersChanged", QJsonValue(true));
        QJsonObject jOnline;
        int currentOnline = m_servers[originalServerName][originalChannelName].size();
        if (currentOnline > 0) currentOnline -= 1;
        jOnline.insert("count", QJsonValue(currentOnline));
        QJsonArray jOnlineList;
        for (const auto& user: m_servers[originalServerName][originalChannelName]) {
            if (QRegularExpression("^(.*;|~|@|\\&|\\+)?"+m_botNick[originalServerName]+"$").match(user).hasMatch()) {
                continue;
            }
            jOnlineList.push_back(QJsonValue(user));
        }
        jOnline.insert("list", jOnlineList);
        jResponse.insert("online", jOnline);
    } else {
        jResponse.insert("onlineUsersChanged", QJsonValue(false));
    }
    // new messages
    bool newMessagesIsExisted {false};
    QString channelId {server+channel};
    QJsonArray jNewMessages;
    qint64 newLastMessageId {userMessageLastId};

    if (not m_messageCache.contains(channelId)) {
        m_messageCache.insert(channelId, new MessagePool);
        consoleLog("Message caching enabled for "+server+"/#"+channel+". Real time reading started.");
    }
    else {
        auto messages = *(m_messageCache[channelId]->getMessages());
        for (const auto& msg: messages) {
            if (userMessageLastId < msg.first) {
                if (not newMessagesIsExisted) newMessagesIsExisted = true;
                QJsonObject jOneNewMessage;
                jOneNewMessage.insert("user", msg.second->getSender());
                jOneNewMessage.insert("text", msg.second->getText());
                jNewMessages.push_back(jOneNewMessage);
                newLastMessageId = msg.first;
            }
        }
    }

    if (newMessagesIsExisted) {
        jResponse.insert("LMIdChanged" /* last message id == LMId */, QJsonValue(true));
        jResponse.insert("newMessages", jNewMessages);
        jResponse.insert("LMId", QJsonValue(QString::number(newLastMessageId)));
    } else {
        jResponse.insert("LMIdChanged", QJsonValue(false));
    }

    //// Finish
    QByteArray response {QJsonDocument(jResponse).toJson()};
    QString header = HEADER_JSON;
    replaceTag(header, "SIZE", QString::number(response.size()));
    socket->write(header.toUtf8());
    socket->write(response);
}

/*\
|*| //////////'''''''''''''\\\\\\\\\\
|*| HAPPY NEW YEAR IN RUSSIAN PRISON!
|*| ||||||||||    2022     ||||||||||
|*| \\\\\\\\\\_____________//////////
\*/

Message::Message(const QString& s, const QString& t, qint64 timestamp, QObject *parent) :
    QObject(parent),
    m_sender(s),
    m_text(t),
    m_timestamp(timestamp)
{
    connect (&m_selfKiller, &QTimer::timeout, [&](){emit outDated(m_timestamp);});
    m_selfKiller.setSingleShot(true);
    m_selfKiller.start(MSECS_TO_AUTOREMOVE_MESSAGES_FROM_BUFFER);
}

const QString Message::getSender()
{
    return m_sender;
}

const QString Message::getText()
{
    return m_text;
}

MessagePool::MessagePool(QObject *parent) :
    QObject(parent),
    m_lastPing(QDateTime::currentMSecsSinceEpoch())
{}

void MessagePool::saveNewMessage(const QString &nick, const QString &text)
{
    qint64 timestamp = QDateTime::currentMSecsSinceEpoch();
    Message* newMessage = new Message (nick, text, timestamp);
    connect (newMessage, SIGNAL(outDated(qint64)), this, SLOT (messageToDelete(qint64)));
    m_messages.insert({timestamp, newMessage});
}

const std::multimap<qint64, Message *>* MessagePool::getMessages()
{
    m_lastPing = QDateTime::currentMSecsSinceEpoch();
    return &m_messages;
}

qint64 MessagePool::getLastPing()
{
    return m_lastPing;
}

void MessagePool::messageToDelete(qint64 timestamp)
{
    while (m_messages.find(timestamp) != m_messages.end()) {
        auto it = m_messages.find(timestamp);
        it->second->deleteLater();
        m_messages.erase(it);
    }
}

const QString &NickColorist::getGreenColor(bool next)
{
    if (not next) {
        return *m_currentGreen;
    }

    if (m_currentGreen == &GREEN_1) {
        m_currentGreen = &GREEN_2;
    }else{
        m_currentGreen = &GREEN_1;
    }
    return *m_currentGreen;
}

const QString &NickColorist::getRedColor(bool next)
{
    if (not next) {
        return *m_currentRed;
    }

    if (m_currentRed == &RED_1) {
        m_currentRed = &RED_2;
    }else{
        m_currentRed = &RED_1;
    }
    return *m_currentRed;
}

void RequestCounter::operator++()
{
    if (m_lastUpdateDate != QDate::currentDate()) {
        m_requestsCounter = 1;
        m_lastUpdateDate = QDate::currentDate();
    }else{
        m_requestsCounter++;
    }
}

quint64 *RequestCounter::value()
{
    return &m_requestsCounter;
}
